Sistemas Operacionais

Thiago Pires

Fundamentos de Sistemas Operacionais


A função do sistema operacional é fornecer aos programas do usuário um modelo do computador melhor, mais simples e mais limpo, assim como lidar com o gerenciamento de todos os recursos mencionados. A maioria dos computadores tem dois modos de operação: modo núcleo e modo usuário.

Fundamentos de Sistemas Operacionais


O sistema operacional opera em modo núcleo (também chamado modo supervisor). Nesse modo, ele tem acesso completo a todo o hardware e *pode executar qualquer instrução que a máquina for capaz de executar.*


Observe, na figura a seguir, uma visão geral simplificada dos principais componentes:

Fundamentos de Sistemas Operacionais


O resto do software opera em modo usuário, no qual apenas um subconjunto das instruções da máquina está disponível.

A diferença entre os modos exerce papel crucial na maneira como os sistemas operacionais funcionam.

Os sistemas operacionais são enormes, complexos e têm vida longa. O código-fonte do coração de um sistema operacional como Linux ou Windows tem cerca de cinco milhões de linhas.

O que é um sistema operacional?


Os sistemas operacionais realizam duas funções essencialmente não relacionadas: (1) fornecer a programadores de aplicativos (e programas aplicativos, claro) um conjunto de recursos abstratos limpo em vez de recursos confusos de hardware, e (2) gerenciar esses recursos de hardware

O que é um sistema operacional?


Sistemas operacionais transformam o feio em belo, como mostrado na figura:

Usando essa abstração, os programas podem criar, escrever, ler arquivos, sem ter que lidar com detalhes complexos de como o hardware funciona

O que é um sistema operacional?


O conceito de um sistema operacional como fundamentalmente fornecendo abstrações para programas aplicativos é uma visão top-down (abstração de cima para baixo). Uma visão alternativa, bottom-up (abstração de baixo para cima), sustenta que o sistema operacional está ali para gerenciar todas as partes de um sistema complexo.

O que é um sistema operacional?


O gerenciamento de recursos inclui a multiplexação (compartilhamento) de recursos de duas maneiras diferentes: no tempo e no espaço.

  • Quando um recurso é multiplexado no tempo, diferentes programas ou usuários se revezam usando-o.
  • O outro tipo é a multiplexação de espaço. Em vez de os clientes se revezarem, cada um tem direito a uma parte do recurso.

O que é um sistema operacional?


  1. Quando múltiplas saídas de impressão estão na fila para serem impressas em uma única impressora, uma decisão tem de ser tomada sobre qual deve ser impressa em seguida.

Multiplexado no tempo

  1. Em vez de os clientes se revezarem, cada um tem direito a uma parte do recurso. Por exemplo, a memória principal é normalmente dividida entre vários programas sendo executados, de modo que cada um pode ser residente ao mesmo tempo (por exemplo, a fim de se revezar usando a CPU).

Multiplexado no espaço

Revisão sobre hardware de computadores


Um sistema operacional está intimamente ligado ao hardware do computador no qual ele é executado


A CPU, memória e dispositivos de E/S estão todos conectados por um sistema de barramento (bus) e comunicam-se uns com os outros sobre ele.

Revisão sobre hardware de computadores


Processadores

O “cérebro” do computador é a CPU. O ciclo básico de toda CPU é buscar a primeira instrução da memória, decodificá-la para determinar o seu tipo e operandos, executá-la.


Cada CPU tem um conjunto específico de instruções que ela consegue executar. Desse modo, um processador x86 não pode executar programas ARM.

As CPUs têm alguns registradores internos para armazenamento de variáveis e resultados temporários.

Os resgistradores são importantes por causa da multiplexação de tempo da CPU.

Revisão sobre hardware de computadores


Memória

É o segundo principal componente em qualquer computador, o qual deve ser rápido ao extremo (mais rápida do que executar uma instrução, de maneira que a CPU não seja atrasada pela memória).


Memória

A camada superior consiste em registradores internos à CPU. Eles são feitos do mesmo material que a CPU e são, desse modo, tão rápidos quanto ela

Memória cache é uma parte da CPU. Atua como memória temporária para que seja recuperado rapidamente os dados, sem a necessidade de uma busca direta na memória principal

Mémoria principal tem por finalidade o armazenamento de instruções e dados de programas que serão ou estão sendo executados pela CPU.

Discos magnéticos são um tipo de memória não volátil de grande capacidade de armazenamento, usada para guardar informações (instruções e dados de programas) que não serão imediatamente usadas pela CPU.

Exemplo de utilização de cache


DuckDB tem uma forma de consulta (vectorized or just-in-time query execution engines) que são processadas em lotes de dados que consistem em coleções de vetores, cada um contendo uma quantidade fixa de valores das colunas.

O resultado é um uso eficiente das operações no cache, mantendo os dados nas consultas tanto quanto possível no cache L1 e L2 muito rápido.

O zoológico dos sistemas operacionais

  • Sistemas operacionais de computadores de grande porte, de alto desempenho e alta disponibilidade (mainframes)
  • Sistemas operacionais de servidores
  • Sistemas operacionais de computadores pessoais
  • Sistemas operacionais de computadores portáteis
  • Sistemas operacionais embarcados
  • Sistemas operacionais de tempo real

Conceitos de sistemas operacionais

  • Processos: um processo é basicamente um programa em execução e associado a cada processo está um espaço de endereçamento, uma lista de posições de mémoria que vai de 0 a algum máximo, onde o processo pode ler e escrever.
  • Espaços de endereçamento: diz respeito ao gerenciamento e à proteção da memória principal do computador, quando se tem multiplos processos sendo executados.
  • Arquivos: um sistema de arquivos é uma estrutura usada por um sistema operacional para organizar e gerenciar arquivos em um dispositivo de armazenamento. Chamadas do sistemas são necessárias para criar, remover, ler escrever arquivos. Existe o conceito de diretório como uma maneira de agrupar os arquivos

Conceitos de sistemas operacionais

  • Entrada/Saída: o sistema operacional tem um subsistema de E/S para gerenciamento dos dispositivos.
  • Proteção: o sistema operacional gerencia a segurança do sistema de maneira que os arquivos, por exemplo, sejam acessíveis somente por usuários autorizados.
  • Interpretador de comandos (shell): é a principal interface entre um usuário e o sistema operacional.

Conceitos de sistemas operacionais

Memória virtual: A memória virtual proporciona a capacidade de executar programas maiores do que a memória física da máquina, rapidamente movendo pedaços entre a memória RAM e o disco.


Quando se instala o linux é possível definir o tamanho da partição do disco que será utilizado pela memória virtual (swap).

free
              total        used        free      shared  buff/cache   available
Mem:        13290480      480388    10480984        1252     2329108    12527384
Swap:              0           0           0

Chamadas de sistema

Já vimos que os sistemas operacionais apresentam duas funções: abstrações para os usuários e gerenciamento de recursos.

  • A parte do gerenciamento de recursos fica transparente para os usuários e é feita automaticamente.
  • Na sua maior parte a interação entre programas de usuários e o sistema operacional lida com as abstrações.

Chamadas de sistema


A manipulação de uma aplicação de usuário que invoca a chamada de sistema open()
  1. A função open() é executada em modo usuário
  2. No modo núcleo/kernel é feita uma busca em um vetor de endereços a implementação da chamada open()
  3. O open() é executado e retorna o resultado para o sistema operacional e sequencialmente para o usuário

Chamadas de sistema

API: elemento que proporciona uma ligação física ou lógica entre dois sistemas ou partes de um sistema que não poderiam ser conectados diretamente.

  • API define um conjunto de normas que possibilita a comunicação entre plataformas por meio de uma série de padrões e protocolos.
  • Por meio de APIs, desenvolvedores podem criar novos softwares e aplicativos capazes de se comunicar com outras plataformas. Por exemplo: caso um desenvolvedor queira criar um aplicativo de fotos para Android, ele poderá ter acesso à câmera do celular através da API do sistema operacional, sem ter a necessidade de criar uma nova interface de câmera do zero..


Aqui uma abstração da complexidade!

Chamadas de sistema

As chamadas de sistema sempre foram o meio pelo qual os programas de espaço do usuário podem acessar os serviços do kernel

flowchart LR;
    subgraph U["Usuário"]
    A(Função <br> mkdir) --> B[Interface de Chamada de Sistema <br> sys_mkdir];
    end
    subgraph K["Kernel"]
    B --> C[Manipulador de <br> Chamada de Sistema];
    end

    style U fill:#ffffff
    style K fill:#e4e4e4

  • Cria um novo diretório (pasta)
mkdir mynewdir
  • Visualizar a interface de chamada
cat /usr/include/asm*/unistd.h | grep -B 1 sys_mkdir
#define __NR_mkdirat 34
__SYSCALL(__NR_mkdirat, sys_mkdirat)
  • Visualizar os registradores onde são armazenados as info da chamada
cpuid -1 -r | head -3
CPU:
   0x00000000 0x00: eax=0x0000000d ebx=0x756e6547 ecx=0x6c65746e edx=0x49656e69
   0x00000001 0x00: eax=0x000406f0 ebx=0x01020800 ecx=0xfefa3203 edx=0x1f8bfbff

Chamadas de sistema

Uma chamada de sistem pode necessitar de outras chamadas serem realizadas

strace -c mkdir mynewdir
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         5           read
  0.00    0.000000           0         8           close
  0.00    0.000000           0        18           mmap
  0.00    0.000000           0         7           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         4           pread64
  0.00    0.000000           0         2         2 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           mkdir
  0.00    0.000000           0         2         2 statfs
  0.00    0.000000           0         2         1 arch_prctl
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0        26        20 openat
  0.00    0.000000           0        26        20 newfstatat
  0.00    0.000000           0         1           set_robust_list
  0.00    0.000000           0         1           prlimit64
  0.00    0.000000           0         1           getrandom
  0.00    0.000000           0         1           rseq
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000           0       111        45 total

Chamadas de sistema


Os desenvolvedores de aplicações projetam programas de acordo com uma interface de programação de aplicações (API — application programming interface).

A API especifica um conjunto de funções que estão disponíveis para um programador de aplicações, incluindo os parâmetros que são passados a cada função e os valores de retorno que o programador pode esperar.

Portable Operating System Interface (POSIX) é uma API para o sistema UNIX/Linux.

Se usarmos o comando ps, ele deverá se comportar da mesma forma no OpenBSD, Debian e macOS.

Chamadas de sistema


  • Uso da função write em C++
write.cpp
#include<unistd.h>

int main() 
{
    write(1, "Ola Mundo!", 10);
}
  • Nas linguagens de programação C e C++, unistd.h é o nome do arquivo de cabeçalho que fornece acesso à API do sistema operacional POSIX.
  • Compilar: g++ write.cpp
  • Executar: ./a.out

Chamada de sistema


Abaixo a chamada de sistema write sendo utilizada

strace -o trace.txt ./a.out

cat trace.txt | grep -A 5 write
write(1, "Ola Mundo!", 10)              = 10
exit_group(0)                           = ?
+++ exited with 0 +++

Chamada de sistema


No fim das contas um printf realiza a mesma chamada de sistema write

write.cpp
#include<stdio.h>

int main() 
{
    printf("Ola Mundo!");
}
strace -o trace.txt ./a.out

cat trace.txt | grep -A 5 write
write(1, "Ola Mundo!", 10)              = 10
exit_group(0)                           = ?
+++ exited with 0 +++

Chamada de sistema


strace -o trace.txt python -c 'print("Ola Mundo!")'

cat trace.txt | grep -A 5 write 
  • Será realizada a chamada de sistema write?
  • Comparar com as chamadas de sistema feitas com C++

Chamadas de sistema

  • Chamadas de sistema para gerenciamento de processos
  • Chamadas de sistema para gerenciamento de arquivos
  • Chamadas de sistema para gerenciamento de diretórios
  • Chamadas de sistema diversas

Chamadas de sistema para gerenciamento de processos


Chamada Descrição
pid = fork() Criar um processo filho idêntico ao pai
pid = waitpid(pid, statloc, options) Espera que um processo filho seja concluído
s = execve(name, argv, environp) Substitui a imagem do núcleo de um processo
exit(status) Conclui a execução do processo e devolve status

Chamadas de sistema para gerenciamento de processos

fork

fork_exemplo.py
import os
import sys

pid = os.fork()

if pid < 0:
  sys.exit("Fork fail")

print(f"Hello world!, process_id(pid) = {os.getpid()}\n")

# Hello world!, process_id(pid) = 7534
# Hello world!, process_id(pid) = 7536
fork_exemplo.cpp
#include <stdio.h>
#include <unistd.h>

int main()
{
    pid = fork();
    if(pid < 0){
    perror("Fork fail");
    _exit(1);
    }
    printf("Hello world!, process_id(pid) = %d \n", getpid());
    return 0;
}

// Hello world!, process_id(pid) = 685 
// Hello world!, process_id(pid) = 686

Chamadas de sistema para gerenciamento de processos

waitpid

waitpid_exemplo.py
import os

i = 1
waitpid_return = ()
print(f"process_id(pid) parent= {os.getpid()}\n")

pid = os.fork()
if pid > 0:
  for i in range(1, 6):
    waitpid_return = os.waitpid(pid, os.WNOHANG)
    print(f"{i}, process_id(pid) child = {pid}, waitpid = {waitpid_return}\n")
else:
  print(f"{i}, process_id(pid) child = {pid}, waitpid = {waitpid_return}\n")
waitpid_exemplo.cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int i = 1;
int pid;
int waitpid_return;
int main ()
{   
    printf("process_id(pid) parent= %d\n", getpid());
    pid = fork();
    if (pid != 0)
    {   
      
      for (int i = 1; i < 5; i++) {
        waitpid_return = waitpid(pid, NULL, WNOHANG);
        printf("%d, process_id(pid) child = %d, waitpid = %d\n", i, pid, waitpid_return);
      }
    }
    else {
        waitpid_return = waitpid(pid, NULL, WNOHANG);
        printf("%d, process_id(pid) child = %d, waitpid = %d\n", i, pid, waitpid_return);}
        return 0;
    }

Chamadas de sistema para gerenciamento de processos

execve

execve.cpp
#include <unistd.h>
#include <iostream>
#include <string>

std::string c = "/bin/ls";
std::string p = "-lh";
char *args[] = {c.data(), p.data()};
int main() {
  execve(args[0], args, NULL);
}

Chamadas de sistema para gerenciamento de diretórios


Chamada Descrição
s = mkdir(name, mode) Cria um novo diretório
s = rmdir(name) Remove um diretório
s = link(name1, name2) Cria um nova entrada name1 apontando para name2
s = unlink(name) Remove uma entrada de diretório
s = mount(special, name, flag) Monta um sistema de arquivos
s = unmount(special) Desmonta um sistema de arquivos

Chamadas de sistema para gerenciamento de diretórios

Exemplos

# Criar um novo diretório
mkdir mynewdir

# Remover um diretório
rmdir mynewdir

# Criar link entre arquivos
link myfile1 myfile2

# Criar link simbólico (atalho)
ln -s myfile1 myfile2

Chamadas de sistema para gerenciamento diversos


Chamada Descrição
s = chdir(dirname) Altera o diretório de trabalho
s = chmod(name, mode) Altera os bits de proteção de um arquivo
s = kill(pid, signal) Envia um sinal para um processo

Chamadas de sistema para gerenciamento diversos

Exemplos

# Alterar um diretório (geralmente se usa cd)
chdir <diretório existente>

# Define a proteção de um arquivo
chmod 700 myfile

# "Matar" um processo
kill <pid>

Proteção de um arquivo

Devo utilizar um valor octal para usuário/grupo/outros

Octal Binário Código de Proteção
0 000 Nenhuma permissão ---
1 001 Permissão de execução --x
2 010 Permissão de escrita -w-
3 011 Permissão de escrita e execução -wx
4 100 Permissão de leitura r--
5 101 Permissão de leitura e execução r-x
6 110 Permissão de leitura e escrita rw-
7 111 Todas as permissões rwx
# Permissão de escrita para o usuário (2) e nenhuma permissão para o grupo (0) e outros (0)
chmod 200 myfile
ls -l myfile
--w-------  1 thop  staff  0 27 Mar 16:18 myfile
# Permissão de leitura/escrita para o usuário (6) e permissão de leitura para o grupo (4)
chmod 640 myfile
ls -l myfile
-rw-r-----  1 thop  staff  0 27 Mar 16:18 myfile

Estruturas do Sistema Operacional

Os sistemas operacionais podem ser organizados de várias maneiras, dependendo de sua estrutura interna e de como eles gerenciam os recursos do computador.

Monolítico


  • Nesta estrutura, todo o sistema operacional é implementado como um único programa de grande porte.
  • Todas as funcionalidades do sistema operacional, como gerenciamento de memória, gerenciamento de processos e sistemas de arquivos, residem no kernel.
  • Exemplos incluem sistemas operacionais mais antigos, como MS-DOS e versões mais antigas do UNIX.

Microkernel


  • Neste modelo, o kernel é mínimo e fornece apenas as funcionalidades básicas, como gerenciamento de memória, comunicação entre processos e escalonamento de CPU.
  • Funcionalidades adicionais, como sistemas de arquivos e drivers de dispositivos, são implementadas como processos de usuário que se comunicam com o kernel.
  • Exemplos incluem o MINIX e o Symbian.

Desse modo, um erro no driver de áudio fará que o som fique truncado ou pare, mas não derrubará o computador.

Híbrido


  • Esta abordagem combina elementos do kernel monolítico e do microkernel.
  • O kernel contém algumas funcionalidades essenciais, como gerenciamento de memória e escalonamento de processos, enquanto outras funcionalidades são implementadas como módulos do kernel ou como processos de usuário.
  • Exemplos incluem Linux, Windows NT/2000/XP/Vista/7/8/10.

Sistemas Virtuais


  • Esta estrutura cria uma máquina virtual que simula uma arquitetura de hardware para cada processo.
  • Cada processo tem a ilusão de que possui seu próprio sistema operacional.
  • Exemplos incluem máquinas virtuais como VMware, VirtualBox e Hyper-V.

(a) Um hipervisor de tipo 1. (b) Um hipervisor de tipo 2 puro. (c) Um hipervisor de tipo 2 na prática. Uma discussão sobre a diferença entre hipervisors 1 e 2: https://aws.amazon.com/pt/compare/the-difference-between-type-1-and-type-2-hypervisors/

Gerenciamento de processos

O conceito mais central em qualquer sistema operacional é o processo: uma abstração de um programa em execução.

Contador de Programa e Registrador de Instruções


  • Contador de Programa (Program Counter), é um registrador de propósito especial usado pelo processador para armazenar o endereço da próxima instrução a ser executada.
  • Registrador de Instruções (Instruction Register) contém a instrução que está sendo executada pela CPU

Ciclo de instrução básico

Início da execução do programa:

  1. O Contador de Programa contém 0x00000000 (digamos que este seja o endereço de início do programa na memória).
  2. A instrução codificada é buscada na memória e colocada no Registrador de Instruções.
  3. A instrução é decodificada e executada.
  4. Agora é hora de avançar para o próximo endereço de instrução, e seguir o mesmo processo (ciclo de intrução)

Contador de Programa e Registrador de Instruções


Processos

  • Um processo é apenas uma instância de um programa em execução, incluindo os valores atuais do contador do programa, registradores e variáveis.
  • Processos podem ser criados e terminados dinamicamente.
  • Cada processo tem seu próprio espaço de endereçamento.

O modelo de processo

  1. Vemos um computador multiprogramando quatro programas na memória (com contador programa físico).
  2. Vemos quatro processos, cada um com seu próprio fluxo de controle e sendo executado independente dos outros (contador de programa lógico).
  3. Vemos que, analisados durante um intervalo longo o suficiente, todos os processos tiveram progresso, mas a qualquer dado instante apenas um está sendo de fato executado.


Em qualquer sistema de multiprogramação, a CPU muda de um processo para outro rapidamente, executando cada um por dezenas ou centenas de milissegundos, dando a ilusão do paralelismo.

Criação de processos

Sistemas operacionais precisam de alguma maneira criar processos.

Quatro eventos principais fazem com que os processos sejam criados:

Inicialização do sistema. Alguns processos são de (1) primeiro plano, sendo processos que interagem com o usuário; (2) segundo plano, processos que não estão associados a um usuário (daemons)

Execução de uma chamada de sistema de criação de processo por um processo em execução. Um processo em execução emitirá chamadas de sistema para criar processos novos para ajudá-lo em seu trabalho

Solicitação de um usuário para criar um novo processo. Em sistemas interativos, os usuários podem começar um programa digitando um comando ou clicando duas vezes sobre um ícone.

Início de uma tarefa em lote. Pense no gerenciamento de estoque ao fim de um dia em uma cadeia de lojas, nesse caso usuários podem submeter tarefas em lote ao servidor (possivelmente de maneira remota).

Criação de processos

No UNIX, há apenas uma chamada de sistema para criar um novo processo: fork. Essa chamada cria um clone exato do processo que a chamou. Após a fork, os dois processos, o pai e o filho, têm a mesma imagem de memória, as mesmas variáveis de ambiente e os mesmos arquivos abertos. Mas atuam de forma independente.

Como identificar processos filho?

child.sh
#!/bin/bash
sleep infinity
parent.sh
#!/bin/bash
./child.sh &
sleep infinity
$
parent.sh &

Usando:

$
pgrep -P <parent pid>
pgrep -lP <parent pid>
pstree -p <parent pid>
ps --ppid <parent pid>
ls /proc/<parent pid>/task
cat /proc/<parent pid>/task/<parent pid>/children

Término de processos

Após um processo ter sido criado, ele começa a ser executado e realiza qualquer que seja o seu trabalho. No entanto, nada dura para sempre, nem mesmo os processos. Cedo ou tarde, o novo processo terminará, normalmente devido a uma das condições a seguir:

  1. Saída normal (voluntária).
  2. Erro fatal (involuntário).
  3. Saída por erro (voluntária).
  4. Morto por outro processo (involuntário).

Exemplos:

  1. ctrl + c para interromper programas ou processos no primeiro plano
  2. !g++ foo.cpp e foo.cpp não existe no diretório
  3. Um Erro interno do processo que afeta seu funcionamento, então o usuário é avisado e o programa termina
  4. kill <pid>

Hierarquias de processos

  • Em alguns sistemas, quando um processo cria outro, o processo pai e o processo filho continuam a ser associados de certas maneiras.
  • O processo filho pode em si criar mais processos, formando uma hierarquia de processos.
$
./grandparent.sh
ps -efj | egrep "PGID|children|parent"
kill -9 -<pgid>
grandparent.sh
#!/bin/bash
bash parent.sh &
bash parent.sh &
for i in {1..100}; do 
sleep 2; 
echo -n "This is the grandparent process $i"; 
done
parent.sh
#!/bin/bash
bash children.sh &
bash children.sh &
for i in {1..100}; do sleep 2; echo -n "This is the parent process $i"; 
done
children.sh
#!/bin/bash
for i in {1..100}; do 
sleep 2; 
echo -n "This is a test in children process $i"; 
done

Estados de processos


  • Embora cada processo seja uma entidade independente, com seu próprio contador de programa e estado interno, processos muitas vezes precisam interagir entre si.
  • Um processo pode gerar alguma saída que outro processo usa como entrada.


$
curl -s https://openbible.com/textfiles/kjv.txt | \
grep "Exodus\s2" | \
head -4

São 3 Estados:

  • Em execução (realmente usando a CPU naquele instante).
  • Pronto (executável, temporariamente parado para deixar outro processo ser executado).
  • Bloqueado (incapaz de ser executado até que algum evento externo aconteça).

Estados de processos


  1. O processo é bloqueado aguardando uma entrada
  2. O escalonador seleciona outro processo
  3. O escalonador seleciona esse processo
  4. A entrada torna-se disponível

Um processo pode estar nos estados em execução, bloqueado ou pronto. Transições entre esses estados ocorrem como mostrado.

Estados de processos

O nível mais baixo de um sistema operacional estruturado em processos controla interrupções e escalonamento. Acima desse nível estão processos sequenciais.

O escalonamento, isto é, decidir qual processo deve ser executado, quando e por quanto tempo, é um assunto importante; nós o examinaremos mais adiante neste capítulo. Muitos algoritmos foram desenvolvidos para tentar equilibrar as demandas concorrentes de eficiência para o sistema como um todo e justiça para os processos individuais.

Implementação de processos

Para implementar o modelo de processos, o sistema operacional mantém uma tabela (um arranjo de estruturas) chamada de tabela de processos, com uma entrada para cada um deles.

  • Essas entradas contêm informações importantes sobre o estado do processo quando ele é trocado do estado em execução para pronto ou bloqueado
  • Podendo o processo retornar precisamente para o mesmo estado em que se encontrava antes de ser interrompido.

Alguns dos campos de uma entrada típica na tabela de processos

Scheduling de Processos

Scheduling de Processos


O objetivo da multiprogramação é haver sempre algum processo em execução para maximizar a utilização da CPU.

O compartilhamento de tempo é alternar a CPU entre os processos, com tanta frequência, que os usuários possam interagir com cada programa enquanto ele está sendo executado.

Para alcançar esses objetivos, o scheduler de processos seleciona um processo disponível (possivelmente em um conjunto de vários processos disponíveis) para execução na CPU.

Scheduling de Processos

Cada processo é representado, no sistema operacional, por um bloco de controle de processo (PCB - process control block) - também chamado bloco de controle de tarefa.

Estado do processo pode ser novo, pronto, em execução, em espera, parado, e assim por diante.

Contador do programa. indica o endereço da próxima instrução a ser executada para esse processo.

Registradores da CPU incluem acumuladores, registradores índice, ponteiros de pilhas e registradores de uso geral, além de qualquer informação do código de condição.

Informações de scheduling da CPU incluem a prioridade de um processo, ponteiros para filas de scheduling e quaisquer outros parâmetros de scheduling.

Informações de gerenciamento da memória podem incluir itens como o valor dos registradores base e limite e as tabelas de páginas, ou as tabelas de segmentos.

Fila de scheduling

Quando os processos entram no sistema, eles são inseridos em uma fila de jobs que é composta por todos os processos no sistema. Os processos que estão residindo na memória principal e estão prontos e esperando execução são mantidos em uma lista chamada fila de prontos.

Mudança de contexto


A alocação da CPU a outro processo requer a execução do salvamento do estado do processo corrente e a restauração do estado de um processo diferente. Essa tarefa é conhecida como mudança de contexto


  • As interrupções fazem com que o sistema operacional tire a CPU de sua tarefa corrente para executar uma rotina do kernel.
  • Essas operações ocorrem, com frequência, em sistemas de uso geral.
  • O contexto é representado no PCB do processo.

Mudança de contexto


O tempo gasto na mudança de contexto é puro overhead porque o sistema não executa trabalho útil durante a permuta de processos. A velocidade da permuta varia de uma máquina para outra, dependendo da velocidade da memória, do número de registradores a serem copiados

Comunicação Interprocessos


Os processos que são executados concorrentemente no sistema operacional podem ser processos independentes ou processos cooperativos


Um processo é independente quando não pode afetar outros processos em execução no sistema nem ser afetado por eles. Qualquer processo que não compartilhe dados com outros processos é independente.

Um processo é cooperativo quando pode afetar outros processos em execução no sistema ou pode ser afetado por eles. É claro que qualquer processo que compartilhe dados com outros processos é um processo cooperativo.

Comunicação Interprocessos


Razões para o fornecimento de um ambiente que permita a cooperação entre processos:

Compartilhamento de informações. Já que vários usuários podem estar interessados no mesmo bloco de informações (por exemplo, um arquivo compartilhado), devemos fornecer um ambiente que permita o acesso concorrente.

Aumento da velocidade de computação. Se quisermos que uma tarefa em particular seja executada mais rapidamente, devemos dividi-la em subtarefas a serem executadas em paralelo.

Modularidade. Podemos querer construir o sistema de forma modular, dividindo suas funções em processos

Conveniência. Até mesmo um usuário individual pode trabalhar em muitas tarefas ao mesmo tempo. Por exemplo, um usuário pode editar, ouvir música e compilar em paralelo.

Processos cooperativos


Processos cooperativos demandam um mecanismo de comunicação entre processos (IPC) que lhes permita trocar dados e informações. Há dois modelos básicos de comunicação entre processos: memória compartilhada e transmissão de mensagens.

No modelo de memória compartilhada, estabelece-se uma região da memória que é compartilhada por processos cooperativos. Os processos podem, então, trocar informações lendo e gravando dados na região compartilhada.

No modelo de transmissão de mensagens, a comunicação ocorre por meio de mensagens trocadas entre os sistemas cooperativos.

Sistema de memória compartilhada


A comunicação entre processos que usam memória compartilhada requer que os processos em comunicação estabeleçam uma região de memória compartilhada. Normalmente, a região de memória compartilhada reside no espaço de endereçamento do processo que cria o segmento de memória compartilhada. Outros processos que queiram se comunicar usando esse segmento de memória compartilhada devem anexá-lo ao seu espaço de endereçamento.

Sistema de memória compartilhada


Condições de corrida

Em alguns sistemas operacionais, processos que estão trabalhando juntos podem compartilhar de alguma memória comum que cada um pode ler e escrever.

Veja na figura dois processos querem acessar a memória compartilhada ao mesmo tempo.

Sistema de memória compartilhada


Regiões críticas

Como evitar as condições de corrida? A chave para evitar problemas aqui e em muitas outras situações envolvendo memória compartilhada, arquivos compartilhados e tudo o mais compartilhado é encontrar alguma maneira de proibir mais de um processo de ler e escrever os dados compartilhados ao mesmo tempo.

Colocando a questão em outras palavras, o que precisamos é de exclusão mútua, isto é, alguma maneira de se certificar de que se um processo está usando um arquivo ou variável compartilhados, os outros serão impedidos de realizar a mesma coisa.

Regiões críticas


Quatro condições são estabelecidas para se chegar a uma boa solução de corridas: 1. Dois processos jamais podem estar simultaneamente dentro de suas regiões críticas. 2. Nenhuma suposição pode ser feita a respeito de velocidades ou do número de CPUs. 3. Nenhum processo executando fora de sua região crítica pode bloquear qualquer processo. 4. Nenhum processo deve ser obrigado a esperar eternamente para entrar em sua região crítica.

Exclusão mútua com espera ocupada

Desabilitando interrupções

Com as interrupções desabilitadas, nenhuma interrupção de relógio poderá ocorrer. Afinal de contas, a CPU só é chaveada de processo em processo em consequência de uma interrupção de relógio ou outra, e com as interrupções desligadas, a CPU não será chaveada para outro processo. Então, assim que um processo tiver desabilitado as interrupções, ele poderá examinar e atualizar a memória compartilhada sem medo de que qualquer outro processo interfira.

Contudo, em um sistema multinúcleo (isto é, sistema de multiprocessador) desabilitar as interrupções de uma CPU não evita que outras CPUs interfiram com as operações que a primeira está realizando. Em consequência, esquemas mais sofisticados são necessários.

Exclusão mútua com espera ocupada

Variáveis do tipo trava

Considere ter uma única variável (de trava) compartilhada, inicialmente 0. Quando um processo quer entrar em sua região crítica, ele primeiro testa a trava. Se a trava é 0, o processo a configura para 1 e entra na região crítica. Se a trava já é 1,o processo apenas espera até que ela se torne 0. Desse modo, um 0 significa que nenhum processo está na região crítica, e um 1 significa que algum processo está em sua região crítica.

Infelizmente, essa ideia contém exatamente a mesma falha fatal que vimos no diretório de spool. Suponha que um processo lê a trava e vê que ela é 0. Antes que ele possa configurar a trava para 1, outro processo está escalonado, executa e configura a trava para 1. Quando o primeiro processo executa de novo, ele também configurará a trava para 1, e dois processos estarão nas suas regiões críticas ao mesmo tempo.

Exclusão mútua com espera ocupada

Alternância explícita

A variável do tipo inteiro turn, inicialmente 0, serve para controlar de quem é a vez de entrar na região crítica e examinar ou atualizar a memória compartilhada. Inicialmente, o processo 0 inspeciona turn, descobre que ele é 0 e entra na sua região crítica. O processo 1 também encontra lá o valor 0 e, portanto, espera em um laço fechado testando continuamente turn para ver quando ele vira 1. Testar continuamente uma variável até que algum valor apareça é chamado de espera ocupada.

while True:
  while turn != 1:
    print("processo 0 na região crítica")
    turn = 1
    print("processo 0 na região não crítica")

while True:
  while turn != 0:
    print("processo 1 na região crítica")
    turn = 0
    print("processo 1 na região não crítica")

Digamos que em algum momento os dois processos estão em suas regiões não críticas e o processo 0 colocar turn para 1, mas ele termina o seu processo e precisar retornar para região crítica, mas está impedido.

Exclusão mútua com espera ocupada


Solução de Peterson

Antes de usar as variáveis compartilhadas (isto é, antes de entrar na região crítica), cada processo chama enter_region com seu próprio número de processo, 0 ou 1, como parâmetro. Essa chamada fará que ele espere, se necessário, até que seja seguro entrar. Após haver terminado com as variáveis compartilhadas, o processo chama leave_region para indicar que ele terminou e para permitir que outros processos entrem, se assim desejarem.

Exclusão mútua com espera ocupada


Solução de Peterson

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import threading
import time

cs = 0
flag_0 = False
flag_1 = False
turn = 0

def thread_0():
    global cs, flag_0, flag_1, turn

    flag_0 = True
    turn = 1
    while (flag_1 and turn == 1):
            continue

    for i in range(10):
        cs += 1
        print("Thread 0: cs =", cs)
        time.sleep(0.1)

    flag_0 = False

def thread_1():
    global cs, flag_0, flag_1, turn

    flag_1 = True
    turn = 0
    while (flag_0 and turn == 0):
        continue

    for i in range(10):
        cs += 1000
        print("Thread 1: cs =", cs)
        time.sleep(0.1)

    flag_1 = False

if __name__ == "__main__":
        t0 = threading.Thread(target=thread_0)
        t1 = threading.Thread(target=thread_1)
        t0.start()
        t1.start()

# Reference: https://www.aspires.cc/implementing-peterson-algorithm-with-python/

Sistemas de Transmissão de Mensagens


A transmissão de mensagens fornece um mecanismo para permitir que os processos se comuniquem e sincronizem suas ações sem compartilhar o mesmo espaço de endereçamento. Isso é particularmente útil em um ambiente distribuído em que os processos em comunicação podem residir em diferentes computadores conectados por uma rede. Por exemplo, um programa de bate-papo na Internet poderia ser projetado de modo que os participantes se comuniquem uns com os outros trocando mensagens.

Um recurso de transmissão de mensagens fornece, pelo menos, duas operações:

  • send(message)
  • receive(message)

Sistemas de Transmissão de Mensagens


Sockets

Um socket é definido como uma extremidade de comunicação. Um par de processos comunicando-se por uma rede emprega um par de sockets — um para cada processo. Um socket é identificado por um endereço IP concatenado com um número de porta. Geralmente, os sockets usam uma arquitetura cliente-servidor. O servidor espera solicitações recebidas de clientes ouvindo em uma porta especificada. Uma vez que uma solicitação seja recebida, o servidor aceita uma conexão proveniente do socket do cliente para completá-la.

Sisitemas de Transmissão de Mensagens


server.py
import socket


def run_server():
    # create a socket object
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server_ip = "127.0.0.1"
    port = 8000

    # bind the socket to a specific address and port
    server.bind((server_ip, port))
    # listen for incoming connections
    server.listen(0)
    print(f"Listening on {server_ip}:{port}")

    # accept incoming connections
    client_socket, client_address = server.accept()
    print(f"Accepted connection from {client_address[0]}:{client_address[1]}")

    # receive data from the client
    while True:
        request = client_socket.recv(1024)
        request = request.decode("utf-8") # convert bytes to string
        
        # if we receive "close" from the client, then we break
        # out of the loop and close the conneciton
        if request.lower() == "close":
            # send response to the client which acknowledges that the
            # connection should be closed and break out of the loop
            client_socket.send("closed".encode("utf-8"))
            break

        print(f"Received: {request}")

        response = "accepted".encode("utf-8") # convert string to bytes
        # convert and send accept response to the client
        client_socket.send(response)

    # close connection socket with the client
    client_socket.close()
    print("Connection to client closed")
    # close server socket
    server.close()


run_server()
client.py
import socket


def run_client():
    # create a socket object
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server_ip = "127.0.0.1"  # replace with the server's IP address
    server_port = 8000  # replace with the server's port number
    # establish connection with server
    client.connect((server_ip, server_port))

    while True:
        # input message and send it to the server
        msg = input("Enter message: ")
        client.send(msg.encode("utf-8")[:1024])

        # receive message from the server
        response = client.recv(1024)
        response = response.decode("utf-8")

        # if server sent us "closed" in the payload, we break out of the loop and close our socket
        if response.lower() == "closed":
            break

        print(f"Received: {response}")

    # close client socket (connection to the server)
    client.close()
    print("Connection to server closed")

run_client()


Exemplo extraído daqui

Threads


Cada processo tem um espaço de endereçamento e um único thread de controle. Na realidade, essa é quase a definição de um processo. Não obstante isso, em muitas situações, é desejável ter múltiplos threads de controle no mesmo espaço de endereçamento executando em quase paralelo, como se eles fossem (quase) processos separados (exceto pelo espaço de endereçamento compartilhado).

Threads, miniprocessos


Por que alguém iria querer ter um tipo de processo dentro de um processo? Na realidade, há várias razões para a existência desses miniprocessos, chamados threads

A capacidade para as entidades em paralelo compartilharem um espaço de endereçamento e todos os seus dados entre si.

São mais leves do que os processos, eles são mais fáceis (isto é, mais rápidos) para criar e destruir do que os processos.

Quando há uma computação substancial e também E/S substancial, contar com threads permite que essas atividades se sobreponham.

Threads são úteis em sistemas com múltiplas CPUs, onde o paralelismo real é possível.

Um processador de texto com três threads


Um thread interage com o usuário e o outro lida com a reformatação em segundo plano. Tão logo a frase é apagada da página 1, o thread interativo diz ao de reformatação para reformatar o livro inteiro. Enquanto isso, o thread interativo continua a ouvir o teclado e o mouse e responde a comandos simples como rolar a página 1 enquanto o outro thread está trabalhando com afinco no segundo plano. Um terceiro thread pode fazer backups de disco sem interferir nos outros dois.

Um processador de texto com três threads


Deve ficar claro que ter três processos em separado não funcionaria aqui, pois todos os três threads precisam operar no documento. Ao existirem três threads em vez de três processos, eles compartilham de uma memória comum.

O modelo de thread clássico


  1. vemos três processos tradicionais. Cada processo tem seu próprio espaço de endereçamento e um único thread de controle. Cada um deles opera em um espaço de endereçamento diferente.
  2. vemos um único processo com três threads de controle. Embora em ambos os casos tenhamos três threads. Todos os três compartilham o mesmo espaço de endereçamento.

Todo thread pode acessar todo espaço de endereçamento de memória dentro do espaço de endereçamento do processo, um thread pode ler, escrever, ou mesmo apagar a pilha de outro thread. Não há proteção, porque (1) é impossível e (2) não seria necessário.

Threads POSIX


Para possibilitar que se escrevam programas com threads portáteis, o IEEE definiu um padrão para threads no padrão IEEE 1003.1c. O pacote de threads que ele define é chamado Pthreads.

Todos os threads têm determinadas propriedades. Cada um tem um identificador, um conjunto de registradores (incluindo o contador de programa), e um conjunto de atributos, que são armazenados em uma estrutura.

Algumas das chamadas de função do Pthreads:

Executando Threads


multiple_threads.py

"""
Executando Múltiplas Threads

https://realpython.com/intro-to-python-threading/
"""

import logging
import threading
import time

def thread_function(name):
    logging.info("Thread %s: starting, ID: %s", name, threading.get_native_id())
    time.sleep(2)
    logging.info("Thread %s: finishing", name)

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    threads = list()
    for index in range(3):
        logging.info("Main    : create and start thread %d.", index)
        x = threading.Thread(target=thread_function, args=(index,))
        threads.append(x)
        x.start()

    for index, thread in enumerate(threads):
        logging.info("Main    : before joining thread %d.", index)
        thread.join()
        logging.info("Main    : thread %d done", index)

Convertendo de um thread para multithread

Conflitos entre threads sobre o uso de uma variável global


Conflitos entre threads sobre o uso de uma variável global.

Threads podem ter variáveis globais individuais.

Scheduling da CPU

O scheduling da CPU é a base dos sistemas operacionais multiprogramados. Alternando a CPU entre os processos, o sistema operacional pode tornar o computador mais produtivo.

Ciclo de Picos de CPU e I/O

O sucesso do scheduling da CPU depende de uma propriedade observada nos processos: a execução de processos consiste em um ciclo de execução da CPU e espera por I/O. Os processos se alternam entre esses dois estados. A execução do processo começa com um pico de CPU. Esse é seguido por um pico de I/O, que é seguido por outro pico de CPU e, então, outro pico de I/O, e assim por diante. Eventualmente, o último pico de CPU termina com o sistema solicitando o encerramento da execução

Scheduler da CPU


Sempre que a CPU se torna ociosa, o sistema operacional deve selecionar um dos processos na fila de prontos para ser executado. O processo de seleção é realizado pelo scheduler de curto prazo, ou scheduler da CPU. O scheduler seleciona um processo entre os processos na memória que estão prontos para execução e aloca a CPU a esse processo.

Scheduling com Preempção


Decisões de scheduling da CPU podem ser tomadas sob as seguintes circunstâncias:

  1. Quando um processo passa do estado de execução para o estado de espera por exemplo, como resultado de uma solicitação de I/O ou de uma invocação a wait para o encerramento de um processo-filho
  2. Quando um processo passa do estado de execução para o estado de pronto (por exemplo, quando ocorre uma interrupção)
  3. Quando um processo passa do estado de espera para o estado de pronto (por exemplo, na conclusão de I/O)
  4. Quando um processo termina

Scheduling com Preempção


Nas situações 1 e 4, não há alternativa no que diz respeito ao scheduling. Um novo processo (se existir um na fila de prontos) deve ser selecionado para execução. Há uma alternativa, no entanto, nas situações 2 e 3.

 

Quando o scheduling ocorre apenas sob as circunstâncias 1 e 4, dizemos que o esquema de scheduling é sem preempção ou cooperativo. Caso contrário, ele tem preempção.

Despachante


Outro componente envolvido na função de scheduling da CPU é o despachante. O despachante é o módulo que passa o controle da CPU ao processo selecionado pelo scheduler de curto prazo. Essa função envolve o seguinte:

  • Mudança de contexto
  • Mudança para a modalidade de usuário
  • Salto para a locação apropriada no programa do usuário para que ele seja reiniciado.


O tempo que o despachante leva para interromper um processo e iniciar a execução de outro é conhecido como latência do despacho.

Critérios de Scheduling

Diferentes algoritmos de scheduling da CPU têm diferentes propriedades, e a escolha de um algoritmo específico pode favorecer uma classe de processos em vez de outra. Seguem alguns critérios para comparação de algorítmos:

Utilização da CPU. Queremos manter a CPU tão ocupada quanto possível. Conceitualmente, a utilização da CPU pode variar de 0 a 100%. Em um sistema real, ela deve variar de 40% (para um sistema pouco carregado) a 90% (para um sistema pesadamente carregado).

Throughput. Se a CPU está ocupada executando processos, trabalho está sendo realizado. Uma medida de trabalho é o número de processos que são concluídos por unidade de tempo, chamado throughput. Para processos longos, essa taxa pode ser de um processo por hora; para transações curtas, ela pode ser de dez processos por segundo.

Tempo de turnaround. Do ponto de vista de um processo específico, o critério importante é quanto tempo ele leva para ser executado. O intervalo entre o momento em que o processo é submetido e o momento de sua conclusão é o tempo de turnaround. O tempo de turnaround é a soma dos períodos gastos em espera para entrar na memória, em espera na fila de prontos, em execução na CPU, e executando I/O.

Tempo de espera. O algoritmo de scheduling da CPU não afeta o período de tempo durante o qual um processo é executado ou faz I/O. Ele afeta somente o período de tempo que um processo gasta esperando na fila de prontos. O tempo de espera é a soma dos períodos gastos em espera na fila de prontos.

Tempo de resposta. Em um sistema interativo, o tempo de turnaround pode não ser o melhor critério. Com frequência, um processo consegue produzir alguma saída bem mais cedo e pode continuar computando novos resultados enquanto resultados anteriores estão sendo exibidos para o usuário.

Algoritmos de Scheduling


Scheduling “Primeiro-a-Chegar, Primeiro-a-Ser-Atendido”

O algoritmo mais simples de scheduling da CPU é o algoritmo “primeiro-a-chegar, primeiro-a-ser-atendido” (FCFS – first-come, first-served). - Nesse esquema, o processo que solicita a CPU primeiro é o primeiro a usá-la. - Quando um processo entra na fila de prontos, seu PCB é conectado na cauda da fila. Quando a CPU está livre, ela é alocada ao processo na cabeça da fila. - O lado negativo é que o tempo médio de espera na política FCFS geralmente é bem longo.

Algoritmos de Scheduling


Scheduling Menor-Job-Primeiro

Esse algoritmo associa a cada processo a duração do próximo pico de CPU do processo. Quando a CPU está disponível, ela é atribuída ao processo que tem o próximo pico de CPU mais curto

Algoritmos de Scheduling


Scheduling por Prioridades

Algoritmos de Scheduling


Scheduling Round-Robin

Instalação e Configuração do Linux

Instalação via WSL

Abra o PowerShell e execute

PowerShell
wsl --install

Por padrão será instalada a distribuição Ubuntu.


Caso queira instalar uma outra distribuição adcione o argumento -d e o nome da distribuição

PowerShell
wsl --install -d <nome da distribuição>

Configuração do ambiente


  • Após a instalação será necessário definir um usuário e uma senha
  • Caso você não seja o administrador da máquina, será necessária algumas configurações adicionais:

No PowerShell execute o comando para poder acessar o usuário root:

PowerShell
wsl --install -d <nome da distribuição> -u <nome do usuário>

Por exemplo caso tenha instalado o Debian, será -d Debian

  • Adicionar o seu usuário no sudoers para ter acesso a alguns privilégios

No terminal do linux digitar o comando

$
usermod -aG sudo <nome do usuário>

Instalando programas


Nas distribuições derivadas do Debian o gerenciador de pacotes é o apt

  • Para instalar o programa cmatrix
$
sudo apt install cmatrix

Para executar escreva cmatrix no terminal

$
cmatrix

Erros que podem surgir na instalação


Comandos que podem ser executados para solucionar erros de instalação

sudo apt install <nome do pacote> --fix-missing
sudo apt update
sudo apt --fix-broken install
sudo apt install <nome do pacote>

Alguns programas úteis


  • neovim editor de código
  • g++ compilador de código c++
  • tmux permite que várias sessões de terminal sejam acessadas simultaneamente em uma única janela
  • wget permite fazer download de conteúdos da web

Para instalar todos eles de uma vez

$
sudo apt install neovim g++ tmux wget